/**
 * Copyright 2008 Martin 'Windgazer' Reurings
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package nl.windgazer.maven.plugins.jsmeter;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Locale;
import java.util.jar.JarEntry;

import org.apache.commons.io.IOUtils;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugin.logging.Log;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.JavaScriptException;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;
import org.mozilla.javascript.tools.shell.Global;

/** 
 * This is the JSMeter generator. It's a wrapper that diverts the standard
 * output to the logger and allows you to run JSMeter from within
 * a java environment instead of command-line.<br />
 * It is expected that any Mojo making use of this generator by default
 * exposes all the options outlined below.<br />
 * Developers have access to all the 'options' that JSMeter has and
 * they can be overridden using mavens' plugin configuration.<br />
 * <br />
 *<br />
 *The default JSMeter-plugin configuration:<br />
 *<pre>
 *&lt;plugin>
 *	&lt;groupId>nl.windgazer&lt;/groupId>
 *	&lt;artifactId>jsmeter-plugin&lt;/artifactId>
 *	&lt;version>[VERSION]&lt;/version>
 *	&lt;configuration>
 *		&lt;directory>${project.build.directory}/jsmeter&lt;/directory>
 *		&lt;complexityLimit>10&lt;/complexityLimit>
 *		&lt;recurseLevel>10&lt;/recurse>
 *		&lt;srcDir>${project.warSourceDirectory}&lt;/srcDir>
 *	&lt;/configuration>
 *&lt;/plugin>
 *</pre>
 * @author mreuring
 */
public class JSMeterGenerator {

	private static final String FAILURE_MSG = "Complexity was exceeded in at least 1 function. See output for more detail!";

	private File baseDir = (System.getProperty("basedir")!=null)?new File(System.getProperty("basedir")):null;
	
	/**
     * The temporary directory where JSMeter is stored while running.
     * This is not a JSMeter property, and won't be found in the JSMeter documentation.
     */
	private File tempDir;
	
	/**
     * The target directory where you want the generated doc to end up. This
     * property represents: -d=&lt;PATH> or --directory=&lt;PATH>.
     */
	private File directory;
	
	/**
	 * The level of recursion. Default is 10.
	 */
	private int recurseLevel;
	
	/**
	 * The level of cyclomatic complexity that must not be exceeded. Default is 10.
	 */
	private int complexityLimit;
	
	/**
	 * The directory where we may find the source-files we're making jsmeters for.
	 * This property represents: &lt;SRC_DIR>
	 */
	private File srcDir;

	private Log log;

	public JSMeterGenerator(File tempDir, File target, File src, int complexityLimit, int recurse, Log log) throws JSMeterException {
		String env = System.getProperty("jsmeter.tempdir");
		this.tempDir = (env!=null&&baseDir!=null)?new File(baseDir, env):tempDir;
		env = System.getProperty("jsmeter.directory");
		this.directory = (env!=null&&baseDir!=null)?new File(baseDir, env):target;
		env = System.getProperty("jsmeter.srcdir");
		this.srcDir = (env!=null&&baseDir!=null)?new File(baseDir, env):src;
		env = System.getProperty("jsmeter.complexity");
		this.complexityLimit = (env!=null)?Integer.valueOf(env):complexityLimit;
		env = System.getProperty("jsmeter.recurse");
		this.recurseLevel = (env!=null)?Integer.valueOf(env):recurse;
		this.log = log;
		
		prepToolkit();
	}

	private Log getLog() {
		return log;
	}

    /**
     * Copy an InputStream to an OutputStream.
     * Thank you ludovic.claude54 for providing me with a good update
     * for my crappy piece of code :)
     * (http://code.google.com/p/jsmetertk-plugin/issues/detail?id=1&can=1) 
     * 
     * @param inputStream
     * @param outputStream
     * @return
     * @throws IOException
     */
	private boolean copyStreams(InputStream inputStream, OutputStream outputStream) throws IOException {
		int copied = IOUtils.copy(inputStream, outputStream);
        IOUtils.closeQuietly(inputStream);
        IOUtils.closeQuietly(outputStream);
        return (copied > 0);
	}

	/**
	 * Copy the JSMeter sources from the jar into a working directory.
	 * 
	 * @param from
	 * @param to
	 * @return
	 * @throws IOException
	 */
	private boolean copyToolkit(JarURLConnection from, File to) throws IOException {
		Enumeration<JarEntry> entries = from.getJarFile().entries();
		//Run through the jar-file and extract all the toolkit related files
		while (entries.hasMoreElements()) {
			JarEntry je = entries.nextElement();
			String name = je.getName();
			if (name.startsWith("jsmeter")) {
				if (name.endsWith("/")) {
					//create directories
					String[] dirs = name.split("/");
					File dir = tempDir;
					if (!dir.exists()) {
						getLog().debug("Creating directory '" + dir.getAbsolutePath() + "'");
						dir.mkdir();
						dir.deleteOnExit(); //Automated cleanup
					}
					for (int i = 1; i < dirs.length; i++) {
						dir = new File(dir, dirs[i]);
						if (!dir.exists()) {
							getLog().debug("Creating directory '" + dir.getAbsolutePath() + "'");
							dir.mkdir();
							dir.deleteOnExit(); //Automated cleanup
						}
					}
				}
				else {
					//copy file, assumption is made that directories have been created (due to the order of entries in a jar)
					name = name.substring(name.indexOf("/") + 1);
					File f = new File(tempDir, name);
					if (!f.exists()) {
						getLog().debug("Copy '" + je.getName() + "' to '" + f.getAbsolutePath() + "'");
						InputStream is = getClass().getClassLoader().getResourceAsStream(je.getName()); //'name' String has been modified
						FileOutputStream fo = new FileOutputStream(f);
						copyStreams(is, fo);
						f.deleteOnExit(); //Automated cleanup
					}
				}
			}
		}
		return true;
	}

	/**
	 * Figures out where the toolkit is, where it needs to be stored to run and copies it there (if nessecary).
	 * @throws JSMeterException
	 */
	private void prepToolkit() throws JSMeterException {
		System.setProperty("jsmeter.dir", tempDir.getAbsolutePath()); //Set JSMeter working directory 

		//Copy JSMeter sources to a temporary working directory
		getLog().info("Copying JSMeter sources to temporary directory.");
		URL resource = getClass().getClassLoader().getResource("jsmeter/bin/run.js");
		if (resource == null) throw new JSMeterException("Could not locate JSMeter source files!!!");
		if (resource.getProtocol().startsWith("file")) {
			//In case of an exploded jar, don't bother copying, just set the tempDir
			File fileLoc = new File(resource.getFile());
			tempDir = fileLoc.getParentFile().getParentFile();
		} else {
			try {
				URLConnection uc = resource.openConnection();
				JarURLConnection juc = (JarURLConnection)uc;
				tempDir.mkdirs();
				tempDir.deleteOnExit();
				if (!copyToolkit(juc, tempDir)) new JSMeterException("JSMeter-plugin failed to copy the sources.");
				else getLog().info("JSMeter sources copied to temporary directory, execution will commense shortly.");
			} catch (IOException e) {
				throw new JSMeterException("Failed to obtain JSMeter sources, couldn't read the jar-file.", e);
			}
		}
	}

	/**
	 * Actually runs the JS-code, should not be used directly except for testing purposes.
	 * @param argus
	 * @throws JSMeterException
	 */
	protected void runJSMeterToolkit(final ArrayList<String> argus) throws JSMeterException {
		
		File jsDocApp = new File(tempDir.getAbsolutePath(), "bin/run.js");

		ArrayList<String> finalArgs = new ArrayList<String>(argus);
		finalArgs.add("-j=" + jsDocApp.getAbsolutePath());

		getLog().debug("Final arguments: " + finalArgs.toString());

		//Divert normal and error output to keep mvn results clean
		PrintStream out = System.out;
		PrintStream err = System.err;
		PrintStream alt = new JSMeterInfoStream(new ByteArrayOutputStream(), getLog(), out);
		System.setOut(alt); //Capture all output
		System.setErr(new JSMeterErrorStream(new ByteArrayOutputStream(), getLog(), err, alt)); //Capture any errors ocurring in JSMeter and output them as debug

		try {
			Context cx = Context.enter();
			cx.setLanguageVersion(Context.VERSION_1_6);
            Global global = new Global();
            global.init(cx);
            Scriptable argsObj = cx.newArray(global, finalArgs.toArray(new Object[]{}));
            global.defineProperty("arguments", argsObj, ScriptableObject.DONTENUM);
			FileReader reader = new FileReader(jsDocApp);
			cx.evaluateReader(global, reader, jsDocApp.getName(), 1, null);
		} catch (FileNotFoundException e) {
			throw new JSMeterException("JSMeter startup script could not be located!");
		} catch (IOException e) {
			throw new JSMeterException("Failure to read JSMeter startup script!");
		} finally {
			System.setOut(out);
			System.setErr(err);
		}
	}

	/**
	 * Generate the JSMeter 'report'.
	 * @throws MojoFailureException 
	 */
	protected void generateReport(Locale locale) throws JSMeterException, MojoFailureException {
		if (srcDir.exists()) {
	
			getLog().info("Will be generating JSMeter report for '" + srcDir.getAbsolutePath() + "'.");
	
			//Setting up options/arguments
			ArrayList<String> args = new ArrayList<String>();
			args.add("-d " + directory.getAbsolutePath());
			args.add("-cl " + complexityLimit);
			args.add("-rl " + recurseLevel);
			args.add(srcDir.getAbsolutePath()); //Set last param to the accual source-dir
			try {
				runJSMeterToolkit(args);
			} catch (JavaScriptException jse) {
				if (jse.getMessage().startsWith(FAILURE_MSG)) {
					throw new MojoFailureException(FAILURE_MSG);
				} else {
					throw new JSMeterException(jse);
				}
			}

		} else {
			getLog().info("Will not be generating JSMeter for '" + srcDir.getAbsolutePath() + "', source-directory does not exist.");

		}

	}
}
